﻿using System;
using System.Collections.Generic;
using StockholmsStad.Business.Properties;
using StockholmsStad.Data.Interfaces;
using StockholmsStad.Runtime.Serialization.DataContract;
using StockholmsStad.Runtime.Serialization.DataContract.Enums;
using StockholmsStad.Utilities.Enums;
using StockholmsStad.Utilities.Exceptions;

namespace StockholmsStad.Business
{
    /// <summary>
    /// Hanterar affärslogik för bokningar
    /// </summary>

    public class BookingBusiness : BusinessBase
    {

        /// <summary>
        /// Skapar businessobjekt som inte kräver autentiserad användare
        /// </summary>
        public BookingBusiness()
            : this(new User { Role = UserRole.ReadOnly })
        {
        }        
        
        /// <summary>
        /// Skapar business-objektet
        /// </summary>
        /// <param name="user"></param>
        public BookingBusiness(User user) : base(user)
        {
        }

        ///<summary>
        /// Sparar en bokning, skickar mail med information om bokning. Om bokningen ligger utanför tiden
        /// för hindersprövning kan inte bokningen genomföras. 
        ///</summary>
        ///<param name="ticketId"></param>
        ///<param name="booking"></param>
        ///<exception cref="StockholmStadSystemException">Kastas om bokningen ligger utanför hindersprövning</exception>
        public void SaveBooking(int ticketId, Booking booking)
        {
            if (CurrentUser.Role == UserRole.ReadOnly)
                throw new StockholmStadSystemException(SystemErrorCode.AccessDenied);

            //Vi måste använda ett context för att läsa och ett annat för uppdatera
            //annars får vi en låsning i transaktionen
            IDataContext readContext = GetDataContext(false);

            Ticket ticket = GetTicket(readContext, ticketId);

            if (booking == null)
                throw new StockholmStadSystemException(SystemErrorCode.BookingNotExist);

            if (IsOutsideImpedimentDoc(readContext, ticket, booking))
                throw new StockholmStadSystemException(SystemErrorCode.BookingIsOutsideImpedimentDoc);

            IBookingAgent bookingAgent = AgentFactory.GetBookingAgent();
            Booking oldBooking = bookingAgent.GetBooking(readContext, ticket.TicketId);

            readContext.Dispose();

            using (IDataContext context = GetDataContext(true))
            {
                // Nollställ flagga för emailpåminnelse när en bokning skapas eller då förändring av bokad tid har skett
                ICeremonyAgent ceremonyAgent = AgentFactory.GetCeremonyAgent();
                ceremonyAgent.ResetCeremonyFlags(context, ticketId);

                ITicketAgent ticketAgent = AgentFactory.GetTicketAgent();
                var ticketStatus = TicketStatusType.Preliminary;

                // Spara eller uppdatera bokning
                if (oldBooking == null)
                {
                    AddBooking(context, ticketId, booking);
                }
                else
                {
                    UpdateBooking(context, ticketId, booking);
                }

                //Kontrollera om paret har en giltig HP, isåfall sätter vi status till Confirmed
                if (ticket.Ceremony.ImpedimentDocIssuedDate.HasValue)
                {
                    var propAgent = AgentFactory.GetPropertiesAgent();
                    var properties = propAgent.GetProperties(context);
                    //Dra bort en dag så att tiden inte spelar någon roll vid jämförelsen
                    var earliestValidDate = booking.DateTime.StartDateTime.AddMonths(-properties.ImpedimentDocValidMonths).AddDays(-1);
                    if (ticket.Ceremony.ImpedimentDocIssuedDate.Value > earliestValidDate)
                    {
                        ticketStatus = TicketStatusType.Confirmed;
                    }
                }

                // Uppdatera ticketstatus
                ticketAgent.UpdateTicketStatus(context, ticketId, ticketStatus);

                SendBookingEmail(context, ticket);

                LogBookingInformation(context, ticket, booking);

                //Om bokningen sätts direkt till status bekräftad ska även bekräftelse mail skickas
                if (ticketStatus == TicketStatusType.Confirmed)
                {
                    var ceremonyId = ceremonyAgent.GetCeremonyId(context, ticketId);
                    ceremonyAgent.SendConfirmedBookingMail(context, ceremonyId, ticket, booking.DateTime.StartDateTime, true);
                }

                context.TransactionComplete();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="ticketId"></param>
        public void CancelBooking(int ticketId)
        {
            if (CurrentUser.Role == UserRole.ReadOnly)
                throw new StockholmStadSystemException(SystemErrorCode.AccessDenied);

            using (IDataContext context = GetDataContext(true))
            {
                var ticketAgent = AgentFactory.GetTicketAgent();
                var ticket = ticketAgent.GetTicket(context, ticketId);

                if (ticket != null)
                {
                    var recipient = GetContactAddressForCouple(ticket.Couple);
                    var bookingAgent = AgentFactory.GetBookingAgent();
                    bookingAgent.RemoveBooking(context, ticketId, CurrentUser.Username,
                                               recipient, Resources.PreliminaryBookingCancelled);
                    context.TransactionComplete();
                }
                else
                {
                    throw new StockholmStadSystemException(SystemErrorCode.BookingNotExist);
                }
            }
        }

        /// <summary>
        /// Hämtar alla lediga tidblock från dagens datum om webservice = false
        /// eller 
        /// Hämtar alla lediga tidblock från ett datum framåt i tiden beroende på
        /// om det är en ordinarie eller individuell bokning
        /// </summary>
        /// <param name="webservice">Anger om anropet kommer från webservicen</param>
        /// <param name="ordinary">Anger om det är en ordinarie eller individuell bokning</param>
        /// <returns></returns>
        public List<TimeBlock> GetFreeTimeBlockDateTimes(bool webservice, bool ordinary)
        {
            IBookingAgent bookingAgent = AgentFactory.GetBookingAgent();
            using (IDataContext context = GetDataContext(false))
            {
                DateTime startDate = DateTime.Now;
                //Om vi kommer från webben ska vi ta hänsyn till parametrar
                if (webservice)
                {
                    var agent = AgentFactory.GetPropertiesAgent();
                    ParVisProperties parVisProperties = agent.GetProperties(context);

                    //Kolla vilken parameter som ska användas
                    if (ordinary)
                    {
                        startDate = startDate.AddDays(parVisProperties.OrdinaryPreliminaryBookingEarliestDays);
                    }
                    else
                    {
                        startDate = startDate.AddDays(parVisProperties.IndividualPreliminaryBookingEarliestDays);
                    }
                }
                return bookingAgent.GetFreeTimeBlockDateTimes(context, startDate);
            }
        }

        /// <summary>
        /// Hämtar alla lediga tidblock för ett visst datumintervall
        /// </summary>
        /// <returns></returns>
        public List<TimeBlock> GetFreeTimeBlockDateTimes(DateTime fromDate, DateTime toDate)
        {
            IBookingAgent bookingAgent = AgentFactory.GetBookingAgent();
            using (IDataContext context = GetDataContext(false))
            {
                return bookingAgent.GetFreeTimeBlockDateTimes(context, fromDate, toDate);
            }
        }

        /// <summary>
        /// Hämtar alla bokade dagar, dvs det första tidblocket för en dag
        /// som inte har några lediga tider. Enbart dagar som ligger efter dagens datum returneras
        /// </summary>
        /// <returns></returns>
        public IEnumerable<TimeBlock> GetBookedTimeBlockDates()
        {
            IBookingAgent bookingAgent = AgentFactory.GetBookingAgent();
            using (IDataContext context = GetDataContext(false))
            {
                return bookingAgent.GetBookedTimeBlockDates(context);
            }
        }

        /// <summary>
        /// Hämtar bokning för en användare
        /// </summary>
        /// <returns></returns>
        public Booking GetBooking(int ticketId)
        {
            using (IDataContext context = GetDataContext(false))
            {
                IBookingAgent bookingAgent = AgentFactory.GetBookingAgent();
                return bookingAgent.GetBooking(context, ticketId);
            }
        }
        /// <summary>
        /// Returns true if booking/contact info is updateable from web application
        /// </summary>
        /// <param name="booking"></param>
        /// <returns></returns>
        public bool IsUpdateable(Booking booking)
        {
            if (booking == null)
                return false;

            using (IDataContext context = GetDataContext(true))
            {
                DateTime? ceremonyDate = GetCeremonyDate(booking);
                if (ceremonyDate.HasValue)
                {
                    return ceremonyDate.Value.AddDays(-GetApplicationChangeDateDays(context, booking.CeremonyType)) >=
                           DateTime.Today;
                }
                //Om man inte har någon bokad tid så returneras true för att man ska kunna ändra sina uppgifter samt boka tid
                return true;
            }
        }
        #region private methods

        private void SendPreferredDateNotification(IDataContext context, Ticket ticket, Booking newBooking)
        {
            // Om det inte finns någon tid att frigöra
            if (ticket.TicketId <= 0)
            {
                return;
            }
            IBookingAgent bookingAgent = AgentFactory.GetBookingAgent();
            Booking oldBooking = bookingAgent.GetBooking(context, ticket.TicketId);
            // Om den gamla bokningen inte har någon ordinarie tid
            if (oldBooking.CeremonyType == CeremonyType.Individual || oldBooking.DateTime == null)
            {
                return;
            }
            // borttagning av bokad tid
            if (newBooking == null || newBooking.DateTime == null)
            {
                bookingAgent.SendPreferredDateNotification(context, oldBooking.DateTime.StartDateTime);
                return;
            }
            // Förändrad tid
            if (!newBooking.DateTime.StartDateTime.Equals(oldBooking.DateTime.StartDateTime))
            {
                bookingAgent.SendPreferredDateNotification(context, oldBooking.DateTime.StartDateTime);
            }
        }

        private void SendBookingEmail(IDataContext context, Ticket ticket)
        {
            IBookingAgent bookingAgent = AgentFactory.GetBookingAgent();

            Booking booking = bookingAgent.GetBooking(context, ticket.TicketId);
            
            if (booking == null)  
                throw new ArgumentNullException("booking");

            if (booking.CeremonyType == CeremonyType.Ordinary)
                bookingAgent.SendOrdinaryBookingMail(context, ticket);
            else if (booking.CeremonyType == CeremonyType.Individual)
                bookingAgent.SendIndividualBookingMail(context, ticket);                
        }

        private int GetApplicationChangeDateDays(IDataContext context, CeremonyType ceremonyType)
        {
            IPropertiesAgent propertiesAgent = AgentFactory.GetPropertiesAgent();
            ParVisProperties properties = propertiesAgent.GetProperties(context);
            if (ceremonyType == CeremonyType.Individual)
            {
                return properties.ApplicationChangeDateIndividualBookingDays;
            }
            if (ceremonyType == CeremonyType.Ordinary)
            {
                return properties.ApplicationChangeDateIndividualBookingDays;
            }
            throw new StockholmStadSystemException(SystemErrorCode.BookingMustBeEitherOrdinaryOrIndividual);
        }
        private static DateTime? GetCeremonyDate(Booking booking)
        {
            if (booking.CeremonyType == CeremonyType.Individual)
            {
                return booking.DateTime.StartDateTime;
            }

            if (booking.CeremonyType == CeremonyType.Ordinary)
            {
                if (booking.DateTime != null)
                {
                    return booking.DateTime.StartDateTime;
                }
            }
            return null;
        }

        


        private bool IsOutsideImpedimentDoc(IDataContext context, Ticket ticket, Booking booking)
        {
            if (booking.DateTime == null)
            {
                // Det finns ingen ny bokning att kontrollera
                return false;
            }
            DateTime? issuedDate = ticket.Ceremony.ImpedimentDocIssuedDate;
            if (issuedDate.HasValue)
            {
                var bookingDateTime = booking.DateTime.StartDateTime;
                var bookingDate = new DateTime(bookingDateTime.Year, bookingDateTime.Month, bookingDateTime.Day);
                IPropertiesAgent propertiesAgent = AgentFactory.GetPropertiesAgent();
                ParVisProperties properties = propertiesAgent.GetProperties(context);
                DateTime earliestDate = bookingDate.AddMonths(-properties.ImpedimentDocValidMonths);
                DateTime? latestDate = null;
                if (booking.CeremonyType == CeremonyType.Ordinary)
                {
                    latestDate = bookingDate.AddDays(-properties.OrdinaryImpedimentDocRemoveBookingAtDays);
                }

                if (booking.CeremonyType == CeremonyType.Individual)
                {
                    latestDate = bookingDate.AddDays(-properties.IndividualImpedimentDocRemoveBookingAtDays);
                }
                return !(issuedDate >= earliestDate && issuedDate <= latestDate);
            }
            return false;
        }

        private void LogBookingInformation(IDataContext context, Ticket ticket, Booking booking)
        {
            ITicketAgent ticketAgent = AgentFactory.GetTicketAgent();
            string place;
            string date = "";
            string prefDatesMessage = Resources.WithPreferredDates;
            string message = Resources.PreliminaryBookingConfirmation;

            if (booking.CeremonyType == CeremonyType.Individual)
            {
                place = "på annan plats";
                date = booking.DateTime.StartDateTime.ToString("yyyy-MM-dd");
            }
            else
            {
                place = "i stadshuset";

                //Kontrollera om det finns några önskemål
                if(booking.PreferredDate1.HasValue)
                {
                    date = booking.PreferredDate1.Value.ToString("yyyy-MM-dd");

                    if (booking.PreferredDate2.HasValue)
                        date += " och " + booking.PreferredDate2.Value.ToString("yyyy-MM-dd");

                    message += " " + string.Format(prefDatesMessage, date);
                }

                // Bokningen har endast preferred dates
                if (booking.DateTime == null)
                {
                    // Hämta annat meddelande
                    message = Resources.PreferredDatesBookingConfirmation;
                }
                else
                {
                    date = booking.DateTime.StartDateTime.ToString("yyyy-MM-dd HH:mm");
                }
            }

            ticketAgent.InsertHistory(context, ticket.TicketId,
                string.Format(message, place, date), CurrentUser.Username, true);
        }

        private void UpdateBooking(IDataContext context, int ticketId, Booking booking)
        {
            if (booking == null)
                throw new ArgumentNullException("booking");

            IBookingAgent bookingAgent = AgentFactory.GetBookingAgent();
            if (booking.CeremonyType == CeremonyType.Individual)
            {
                if(string.IsNullOrEmpty(booking.MinisterCode))
                    throw new StockholmStadSystemException(SystemErrorCode.MinisterCodeNotExists);

                int ministerId = GetMinisterId(context, booking.MinisterCode);
                bookingAgent.UpdateBooking(context, ticketId, booking, ministerId, CurrentUser.Username);
            }
            else
            {
                bookingAgent.UpdateBooking(context, ticketId, booking, null, CurrentUser.Username);
            }
        }

        private int AddBooking(IDataContext context, int ticketId, Booking booking)
        {
            if (booking == null)
                throw new ArgumentNullException("booking");

            IBookingAgent bookingAgent = AgentFactory.GetBookingAgent();
            int bookingId;
            if (booking.CeremonyType == CeremonyType.Individual)
            {
                if(string.IsNullOrEmpty(booking.MinisterCode))
                    throw new StockholmStadSystemException(SystemErrorCode.MinisterCodeNotExists);

                int ministerId = GetMinisterId(context, booking.MinisterCode);
                bookingId = bookingAgent.AddBooking(context, ticketId, booking, ministerId);
            }
            else
            {
                if (booking.DateTime != null)
                {
                    bookingId = bookingAgent.AddBooking(context, ticketId, booking, null);
                }
                else
                {
                    // Kontrollera om bokningen enbart innehåller preferred dates.
                    bookingId = booking.DateTime == null ? 
                        bookingAgent.AddBooking(context, ticketId, booking, null) : 
                        bookingAgent.AddBooking(context, ticketId, booking, null);
                }
            }

            return bookingId;
        }

        #region EntityFinders

        private static string GetContactAddressForCouple(Couple couple)
        {
            if(couple.ContactPerson != null && !string.IsNullOrEmpty(couple.ContactPerson.Email))
                return couple.ContactPerson.Email;

            if(couple.Person1 != null && !string.IsNullOrEmpty(couple.Person1.Email))
                return couple.Person1.Email;

            return string.Empty;
        }

        private int GetMinisterId(IDataContext context, string ministerCode)
        {
            IMinisterAgent ministerAgent = AgentFactory.GetMinisterAgent();

            int? ministerId = ministerAgent.GetMinisterIdByMinisterCode(context, ministerCode);
            if (ministerId == null)
                throw new StockholmStadSystemException(SystemErrorCode.MinisterNotExists);
            return ministerId.Value;
        }

        private Ticket GetTicket(IDataContext context, int ticketId)
        {
            ITicketAgent ticketAgent = AgentFactory.GetTicketAgent();
            Ticket ticket = ticketAgent.GetTicket(context, ticketId);
            if (ticket == null)
                throw new StockholmStadSystemException(SystemErrorCode.UserNotExists);
            return ticket;
        }

        #endregion

        #endregion
    }
}